Skip to content

feat(claude-config): give audit-pass an executable for the run state it specified - #2441

Merged
kyle-sexton merged 4 commits into
mainfrom
fix/2280-audit-pass-run-state
Aug 12, 2026
Merged

feat(claude-config): give audit-pass an executable for the run state it specified#2441
kyle-sexton merged 4 commits into
mainfrom
fix/2280-audit-pass-run-state

Conversation

@kyle-sexton

Copy link
Copy Markdown
Contributor

Summary

audit-pass specified a runtime and shipped no executable. SKILL.md plus ten reference/*.md
spell out a lease — path, refresh discipline, a two-sided liveness window, a released tombstone,
owner_epoch fencing — and an epoch-scoped append-only partial that --resume reads instead of the
report. The skill shipped no scripts/ directory at all, the only audit skill in claude-config
without one.

The gap has a sharper form than "no scripts", and it is fully verifiable from the tree:
lib/state-key.sh's own header records the keying scheme as "audit-pass's, reused rather than
reinvented"
and points at skills/audit-pass/reference/run-state-and-resumability.md §3 — and
grep -rl state-key.sh plugins/ returns audit-instructions, audit-prompting-postures and
claude-memory:audit. Every skill called it except the one that specified it.

What this PR adds. skills/audit-pass/scripts/run-state.sh, with subcommands paths,
lease acquire|heartbeat|release|classify, and partial append, plus run-state.test.sh. paths
derives <plugin-data>/runs/<state-key>/<run-id> through lib/state-key.sh, so the skill now runs
on its own scheme. No jq, no GNU-only constructs, no network, no background process.

What it deliberately does not add, and now says so in-contract. Stale-lease adoption (the
owner_epoch compare-and-set) and §7 assembly are not implemented. §3 and §7 now state which
clauses the script enforces and which remain the run's own discipline. The standard this PR is held
to is that a contract must not read as enforced while nothing enforces it — that is satisfied by
implementing or by declaring, and each clause here does one of the two.

Two clauses were reduced rather than implemented, because a skill cannot keep them.

  1. The lease specified a 60-second wall-clock heartbeat. A skill-driven run acts between tool
    calls and has no timer, so that cadence named a mechanism no run could provide — the same defect
    as specifying a lease and shipping no writer. Refresh is now boundary-driven (acquire, each
    lane's persistence point, release). Each lease records the stale_after_s and skew_grace_s its
    writer committed to, and classify reads them from the artifact — which is what the section's own
    "two implementations must reach it identically" concern actually needed. The default threshold
    moves 5 minutes to 30: with boundary-driven refresh a single delegated lane can outlast five
    minutes, and a threshold shorter than a lane classifies a running pass as abandoned, the unsafe
    direction because it lets --resume adopt a live run's artifact.
  2. §5 specified a run manifest carrying per-lane input digest and completion state, beside the
    partial. §7 already required that --resume read the partial "so completion state is derivable
    from the artifact rather than tracked beside it and able to disagree with it" — and a manifest
    beside the partial is precisely what can disagree with it. §5 now states the manifest as the
    partial's own lane records. This closes the second link in F12's chain: making the partial real
    while leaving completion state in a file nothing writes would have moved the defect, not fixed it.

Evidence discipline. F5 is SELF_REPORTED and its "the observed run wrote none" leg is not
reproducible from this repository. Nothing in the code, the CHANGELOG, or this PR asserts it. The
basis for every change here is the specification-versus-implementation gap, which is verifiable
from the tree and was verified.

Trust surface. The script writes only under the plugin data directory it is given — it
discovers no directory — and never inside a target repository, so the report-only contract
(disallowed-tools: Edit, NotebookEdit) is untouched. Both path segments it contributes are
validated: lib/state-key.sh already refuses a remote URL that would become traversing directory
components, and a --run-id outside [A-Za-z0-9][A-Za-z0-9_.-]*, or containing .., is refused
here. Both refusals carry negative tests.

Version: claude-config 0.37.1 to 0.38.0 (new executable surface), with a CHANGELOG entry.

Test plan

Fail-before. The script is new, so main's state is the script absent. With run-state.sh moved
aside, every one of the 52 checks fails:

$ mv run-state.sh ../run-state.sh.hidden && bash run-state.test.sh
FAIL: paths exits 0 on a git target
  detail: expected exit 0, got 127
FAIL: paths echoes the plugin data dir it was given
  detail: expected to contain: plugin_data=/tmp/tmp.Z8gZvUvVaH/plugin-data
FAIL: paths derives a state key through lib/state-key.sh
  detail: expected to contain: state_key=github.com/example/demo/
FAIL: the run dir is keyed under runs/<state-key>/<run-id>
  detail: expected to contain: run_dir=/tmp/tmp.Z8gZvUvVaH/plugin-data/runs/github.com/example/demo/
...

Pass-after.

$ bash plugins/claude-config/skills/audit-pass/scripts/run-state.test.sh
PASS: a run id containing '..' is refused
PASS: the refusal names the traversal
PASS: a run id that is not a plain segment is refused
PASS: without the '..' arm the same id is accepted — the check discriminates
PASS: and it would have assembled a traversing run dir
PASS: without the segment check an absolute id is accepted — the check discriminates
PASS: and it would have escaped the plugin namespace
PASS: a heartbeat beyond the skew grace classifies stale, not live
PASS: and the skew is reported rather than swallowed
PASS: a heartbeat within the skew grace still classifies live
PASS: without the lower bound a dead run pins itself live — the bound discriminates
PASS: the partial is named for the epoch the lease holds
PASS: appends accumulate rather than rewriting the document
PASS: a partial is never written without a lease to classify it
...
All 52 checks passed.

The three negative tests are the point. Each mutates a copy of the script to delete exactly one
check and asserts the mutated copy reaches the outcome the real one refuses — a passing test that
would still pass with the check deleted proves nothing:

  • delete the two-sided window's lower bound, and a heartbeat_at two hours in the future reads
    live instead of stale, so a dead run pins itself live and every --resume refuses an abandoned
    run indefinitely (assertion 3.9);
  • delete the .. arm of --run-id validation, and a..b is accepted and a traversing run dir is
    assembled;
  • delete the segment-shape check, and /etc/passwd is accepted as a run id and escapes the plugin
    namespace.

Each construction is verified before it is trusted: if the sed target no longer matches, the test
fails with "UNVERIFIED by this run" rather than passing vacuously.

Repo gates, run locally:

$ shellcheck --rcfile .shellcheckrc -S info plugins/claude-config/skills/audit-pass/scripts/*.sh
(clean)
$ bash scripts/check-shell-portability.sh --paths .../run-state.sh .../run-state.test.sh
No unexcused GNU-only constructs in 2 shell file(s).
$ bash scripts/check-changed-skills.sh origin/main
INFO: SKILL.md 485/500 lines
INFO: script test passed: scripts/run-state.test.sh
CHECK-SKILL audit-pass: PASS — 0 errors, 2 warning(s)
$ bash scripts/check-changelog-parity.sh --check-bump origin/main
Every plugin whose version changed vs origin/main has a '## [<version>]' CHANGELOG.md entry.
$ bash scripts/check-changelog-parity.sh --check-preserved origin/main
All 1 changed changelog(s) preserve every version heading they carried at da05706 (66 heading(s) compared).
$ bash scripts/check-cross-plugin-source-drift.sh --check
No unregistered or drifted cross-plugin source clusters found.
$ npx markdownlint-cli2 "plugins/claude-config/skills/audit-pass/**/*.md" "plugins/claude-config/CHANGELOG.md"
Summary: 0 issues in 0 files

Every other plugins/claude-config/**/*.test.sh still passes (lib/state-key.test.sh 23,
instruction-scan.test.sh 70, permission-rule-check.test.sh 136, and the rest).

Both SKILL.md warnings are pre-existing: the 200-line soft target, and a stale fresh-eyes-exempt
directive in ## Self-check, a section this PR does not touch.

Related

Closes #2280

Origin: handoff-inbox item 20260811-020411-claude-config-audit-pass-report-path-inside-scan-set
(ledger .work/handoff-inbox-batch-4/ledgers/I8-audit-pass-report-path.md § F3, F5, F12, F13).

Row state, all six:

Row State
F3 — 1,547 lines of contract, zero scripts fixed here: scripts/run-state.sh + run-state.test.sh, three negative tests
F5 — the lease is prose with no executable fixed here: lease acquire|heartbeat|release|classify; the unkeepable 60s cadence reduced to boundary-driven refresh
F12--resume reads a partial nothing writes fixed here: partial append, Phase 4's open terminator written when the handoff is recorded, and §5's separate manifest folded into the partial's lane records
F13 — the disclaimed dimension's mitigation is unimplemented fixed here: the mitigation now names partial append; the disclaimer itself is unchanged, since persistence bounds nothing
F8{id} mangling and the Bash-env fact unstated already fixed at HEAD by ceaf013 (#2403); verified by git log -S and by reading reference/report-location-and-schema.md:25-37
F9${CLAUDE_PROJECT_DIR} "when set" conditional already fixed at HEAD by ceaf013 (#2403); verified the same way at SKILL.md:48-58

Adjacent and deliberately not touched: #1430 (cross-epoch carry-forward), #1940 (rerun-contract.md
design-doc drift), #1568 (${CLAUDE_*} substitution scope). F4's refuse-vs-specify fork is settled
on main already and is not reopened here.

@cursor

cursor Bot commented Aug 12, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 9s —— View job


Security review

  • Gather context (diff, changed files)
  • Review run-state.sh for injection/path-traversal/logic issues
  • Review run-state.test.sh for unsafe patterns
  • Review doc/reference changes for security-relevant claims
  • Post findings

This PR is entirely shell scripts and Markdown (no GitHub Actions workflow changes, so the zizmor-covered lanes — unpinned actions, dangerous triggers, excessive permissions:, template injection — don't apply here).

Scope reviewed: plugins/claude-config/skills/audit-pass/scripts/run-state.sh (new), run-state.test.sh (new), and the SKILL.md / reference-doc diffs that describe how the script is invoked.

IMPORTANT (confidence: plausible) — --run-dir is trusted wholesale by lease * / partial append, unlike paths

cmd_paths (run-state.sh:173-228) carefully validates both path segments it derives--run-id through validate_run_id (run-state.sh:121-133) and --plugin-data for being absolute — before assembling run_dir. But lease acquire/heartbeat/release/classify and partial append (run-state.sh:230-503) all take --run-dir as a raw, pre-built string and never check that it actually falls under <plugin-data>/runs/<state-key>/…. cmd_lease_acquire will happily mkdir -p and write a lease file, and cmd_partial_append will write findings.partial.N.jsonl, at any path handed to --run-dir.

The PR's own "Trust surface" section states the script "writes only under the plugin data directory it is given... and never inside a target repository," and SKILL.md/reference docs instruct the model to always pipe paths' derived run_dir into the later commands (plugins/claude-config/skills/audit-pass/SKILL.md diff, "Do not derive the run directory or hand-write the lease"). That ordering is enforced by prose/model discipline only — nothing in the script itself re-validates that a --run-dir passed to lease/partial actually originated from paths. For a report-only audit skill whose entire premise is scanning potentially adversarial/untrusted repository content with Bash and Write still available, that's exactly the kind of boundary a confused-deputy or prompt-injection scenario could exploit: content in the audited repo that manages to alter the model's next tool call could redirect --run-dir (e.g., into the target repo being audited, or elsewhere under the user's filesystem) and the script would silently mkdir -p/write there — contradicting the explicit "never inside a target repository" guarantee.

Impact is limited (directory creation plus small, fixed-name text files — no code execution, no credential exposure), so this isn't critical, but it's a real gap between the stated trust boundary and what the code mechanically enforces, in a script whose whole purpose (per its own header) is turning previously-prose guarantees into enforced mechanism.

Suggested fix: have lease acquire/heartbeat/release/classify/partial append verify --run-dir is lexically under a --plugin-data-rooted runs/ prefix (or accept --plugin-data+--run-id+--root directly and re-derive the path internally via cmd_paths's own logic) rather than trusting a caller-supplied path verbatim.

Fix this →

No other issues found

  • validate_run_id (run-state.sh:121-133) correctly rejects empty ids, non-[A-Za-z0-9][A-Za-z0-9_.-]* shapes, and any .. substring, with negative tests in run-state.test.sh proving each check is load-bearing (deleting either arm demonstrably lets traversal through). This closes the path-traversal surface for the one input the script treats as untrusted-by-design.
  • No eval, source, command substitution over unsanitized input, or other injection surface — all arguments are consumed as literal argv values ($2, etc.) and never re-interpreted by the shell.
  • --record for partial append is written with printf '%s\n' "$record" >>"$file" — a plain append, not parsed or evaluated by this script, so a malformed/adversarial JSON-shaped string can't do more than corrupt the JSONL artifact's own readability (a data-integrity concern for a downstream reader, not a code-execution or traversal vector here).
  • write_lease_atomic's temp file uses a PID-based name ($dir/.lease.$$) under a directory the script itself creates; this only matters as a symlink race if an attacker already has write access to the same user's plugin-data directory, which is the same trust level as the process itself — not a meaningful privilege boundary, so not flagged as a finding.
  • epoch/stale_after/skew_grace are all validated as non-negative integers (require_positive_int) before being written into the lease or used to build the partial's filename — no injection via those fields.
  • No secrets, tokens, or credentials are handled anywhere in this diff.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dd3b010d76

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/claude-config/skills/audit-pass/scripts/run-state.sh
Comment thread plugins/claude-config/skills/audit-pass/scripts/run-state.sh
Comment thread plugins/claude-config/skills/audit-pass/scripts/run-state.sh
kyle-sexton and others added 2 commits August 12, 2026 08:42
…it specified

`audit-pass` specified a lease (path, refresh discipline, two-sided liveness
window, `released` tombstone, `owner_epoch` fencing) and an append-only partial
across `SKILL.md` and ten `reference/*.md`, and shipped no `scripts/` directory
at all — the only audit skill in the plugin without one. Sharper than "no
scripts": `lib/state-key.sh`, whose header records the keying scheme as
`audit-pass`'s "reused rather than reinvented", was called by three other skills
and never by the one that specified it.

Adds `skills/audit-pass/scripts/run-state.sh` — `paths`, `lease
acquire|heartbeat|release|classify`, `partial append` — with `run-state.test.sh`
carrying three negative tests that delete one check from a copy of the script and
assert the mutated copy reaches the outcome the real one refuses.

Corrects the two clauses a skill-driven run cannot keep: the 60-second wall-clock
heartbeat (a skill has no timer) becomes boundary-driven refresh with the
thresholds recorded in the lease, and §5's separate "run manifest" becomes the
partial's own lane records, which is what §7 already required. Phase 4's `open`
terminator is now appended when the handoff is recorded, so the report's
`--resume` instruction has an artifact to attach to.

Stale-lease adoption and §7 assembly stay unimplemented, and §3/§7 now say so —
a contract that reads as enforced while nothing enforces it is the defect.

Refs #2280 (F3, F5, F12, F13)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…surfaces

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@kyle-sexton
kyle-sexton force-pushed the fix/2280-audit-pass-run-state branch from 3506d53 to afc3486 Compare August 12, 2026 12:46
@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 2s —— View job


Security review

  • Gather context (diff, changed files)
  • Review run-state.sh for injection/path-traversal/logic/trust-boundary issues
  • Review run-state.test.sh for unsafe patterns
  • Review doc/reference changes for security-relevant claims
  • Cross-check existing bot findings (Cursor/Codex) for accuracy
  • Post findings

Scope reviewed: plugins/claude-config/skills/audit-pass/scripts/run-state.sh (new, 557 lines), run-state.test.sh (new), and the SKILL.md / reference/*.md diffs describing how the script is invoked. No workflow/Action files changed, so zizmor's lanes (unpinned actions, dangerous triggers, excessive permissions:, template injection) don't apply. Confirmed the code is unchanged since the prior automated pass on this PR (commit afc3486 only fixed a comment typo, no logic change).

IMPORTANT (confidence: plausible) — --run-dir is trusted verbatim by lease * / partial append, unlike paths

cmd_paths validates both segments it derives--run-id via validate_run_id, --plugin-data for being absolute — before assembling run_dir. But cmd_lease_acquire, heartbeat/release/classify, and cmd_partial_append all take --run-dir as a raw string and never check it falls under <plugin-data>/runs/<state-key>/…. lease acquire will mkdir -p and write a lease file at any --run-dir handed to it (confirmed by reading line 272, no containment check precedes the mkdir -p).

The PR's own "Trust surface" section and the new SKILL.md/reference prose (SKILL.md diff) state the script "writes only under the plugin data directory it is given... and never inside a target repository," but that ordering (always pipe paths' derived run_dir into later commands) is enforced by prompt/model discipline only. For a report-only skill whose whole premise is scanning potentially adversarial repository content with Bash/Write still available, this is a real gap between the stated trust boundary and what the code mechanically enforces: content that manages to influence the model's next tool call could redirect --run-dir and the script would silently mkdir -p/write there. Impact is limited (directory creation + small fixed-name text files, no code execution), so not critical — but it's the same class of "contract in prose, not mechanism" defect this PR's own stated purpose is to close for the rest of the run-state contract.

Suggested fix: have lease */partial append verify --run-dir is lexically under a --plugin-data-rooted runs/ prefix (or take --plugin-data+--run-id and re-derive the path via cmd_paths's own logic) before mkdir -p or writing.

IMPORTANT (confidence: plausible) — lease acquire has no liveness/compare-and-set check, and partial append binds to whatever epoch is currently on disk, not the epoch the caller holds

cmd_lease_acquire never reads or classifies an existing lease before overwriting it — it unconditionally mkdir -ps and calls write_lease_atomic, silently replacing a lease even if classify on it would currently return live. Meanwhile cmd_partial_append re-reads owner_epoch from the lease file at append time rather than pinning to the epoch the caller originally acquired:

epoch=$(lease_field "$run_dir/lease" owner_epoch)
...
file="$run_dir/findings.partial.$epoch.jsonl"

So if a second lease acquire is ever issued against a run directory whose lease is still live (a genuinely concurrent run, a run-id collision, or a --resume flow that skips the classify-before-acquire check the script doesn't enforce — the same "prose orders the calls, code doesn't" gap as the finding above), the second writer's owner_epoch silently becomes the new value in the lease, and any prior holder still calling partial append will have its records appended to the new writer's partial file instead of its own — because the epoch is read live from disk, not carried by the caller. This directly undermines the isolation guarantee the PR describes for epoch-scoped partials ("the partial cannot exist without the lease that classifies it") by letting two writers' records interleave in one JSONL file without error. This overlaps with, and I can independently confirm, Codex's P1 comment on line 496 — the root cause is that acquire performs no compare-and-set against an existing live lease, which the append behavior then compounds.

Exploitability under the documented call pattern (single acquireheartbeat/appendrelease per run) is narrow — it requires either a duplicate/colliding run-id or an out-of-order tool call, not something the normal happy path triggers, and stale-lease adoption is explicitly declared out of scope for this PR. Flagging because the script enforces the two-sided liveness read carefully (the classify negative tests are solid) but the write side of the same mutual-exclusion problem has no equivalent guard, so the isolation property is only as strong as caller discipline, not the script.

Suggested fix: have lease acquire refuse (or require an explicit --adopt flag) when an existing lease at --run-dir currently classifies live, and have partial append take/verify the epoch it expects rather than trusting whatever the lease currently holds.

No other issues found

  • validate_run_id (lines 121-133) correctly rejects empty ids, non-[A-Za-z0-9][A-Za-z0-9_.-]* shapes, and any .. substring; run-state.test.sh's three mutation-based negative tests genuinely prove each check is load-bearing (deleting either arm demonstrably lets traversal/absolute paths through), closing path traversal for the one caller-supplied input the script treats as untrusted-by-design.
  • No eval, source, or command substitution over unsanitized input anywhere in the script; all arguments are consumed as literal argv values.
  • partial append's --record is written with a plain printf '%s\n' "$record" >>"$file" — never parsed or evaluated by this script, so a malformed/adversarial value can at most corrupt the JSONL artifact's own readability for a downstream reader (data-integrity concern, not injection or traversal). The '{'* prefix check (line 486-489) is not full JSON validation (Codex's P2 comment is accurate on this), but I'm not raising it as a security finding — it's an availability/robustness concern for --resume parsing, not an authorization, injection, or trust-boundary gap.
  • write_lease_atomic's temp file ($dir/.lease.$$) is written under a directory the script itself creates; a symlink race there requires an attacker who already has write access to the same user's plugin-data directory, i.e. the same trust level as the process itself — not a meaningful privilege boundary.
  • epoch/stale_after/skew_grace are all validated as non-negative integers via require_positive_int before being written into the lease or used to build the partial's filename — no injection via those fields.
  • No secrets, tokens, or credentials are handled anywhere in this diff.
  • The doc changes (SKILL.md, both reference/*.md files) only describe invocation of the script and make no new security-relevant claims beyond what the script itself does or doesn't enforce; the "Trust surface" language matches the two gaps above but doesn't introduce new ones.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 1m 24s —— View job


Security review

  • Gather context (diff, changed files)
  • Review run-state.sh for injection/path-traversal/logic/trust-boundary issues
  • Review run-state.test.sh for unsafe patterns
  • Review doc/reference changes for security-relevant claims
  • Cross-check prior automated findings for accuracy at current HEAD
  • Post findings

Scope reviewed: plugins/claude-config/skills/audit-pass/scripts/run-state.sh (new), run-state.test.sh (new), and the SKILL.md/reference/*.md diffs describing script invocation, at HEAD 7752f0f. No GitHub Actions/workflow files changed, so zizmor's lanes (unpinned actions, dangerous triggers, excessive permissions:, template injection) don't apply. Verified 7752f0f only changes the executable bit on the two scripts (git show --stat 7752f0f shows 0 insertions, 0 deletions for both files) — the logic is byte-identical to what earlier automated passes reviewed on this PR, so I re-verified those findings directly against the current code rather than assuming they still hold.

IMPORTANT (confidence: plausible) — --run-dir is trusted verbatim by lease * / partial append, unlike paths

cmd_paths validates both path segments it derives--run-id via validate_run_id (line 121) and --plugin-data for being absolute (line 202) — before assembling run_dir. But cmd_lease_acquire, heartbeat/release/classify, and cmd_partial_append all accept --run-dir as a raw string and never check it falls under <plugin-data>/runs/<state-key>/…. Line 272 unconditionally mkdir -p "$run_dir" and writes a lease file at whatever path it's given.

The script's own header ("SCOPE OF WRITES", lines 28-36) and SKILL.md describe writes as confined to the derived run directory, but that ordering — always pipe paths' output into later commands — is enforced only by the calling skill's prose discipline, not by the script. For a report-only audit skill whose whole premise is processing potentially adversarial/untrusted repository content with Bash/Write still available (disallowed-tools: Edit, NotebookEdit only), a --run-dir that gets redirected (e.g. into the audited repo, or elsewhere under the filesystem) via a confused-deputy or prompt-injection-influenced tool call would be honored silently. Impact is limited (directory creation + small fixed-name text files, no code execution, no credential exposure), so not critical — but it's a real gap between the documented trust boundary and what's mechanically enforced.

Suggested fix: have lease */partial append verify --run-dir is lexically contained under a --plugin-data-rooted runs/ prefix (or take --plugin-data+--run-id and re-derive the path via cmd_paths's own logic) before mkdir -p or writing.

Fix this →

IMPORTANT (confidence: plausible) — lease acquire has no liveness check before overwrite, and partial append binds to whatever epoch is currently on disk, not the epoch the caller holds

cmd_lease_acquire never reads or classifies an existing lease before replacing it (line 272 onward) — it unconditionally overwrites even if classify on the existing lease would currently return live. Meanwhile cmd_partial_append reads owner_epoch from the lease file at append time, not the epoch the caller originally acquired:

epoch=$(lease_field "$run_dir/lease" owner_epoch)
...
file="$run_dir/findings.partial.$epoch.jsonl"

If a second lease acquire is ever issued against a run directory whose lease is still live (concurrent run, run-id collision, or a --resume flow that skips a classify-before-acquire check — a check the script itself doesn't enforce, same "prose orders the calls, code doesn't" gap as the finding above), the lease's owner_epoch silently becomes the new value, and any prior holder still calling partial append will have its records appended into the new writer's partial file — because the epoch is read live from disk rather than carried by the caller. This undermines the documented epoch-isolation guarantee ("the partial cannot exist without the lease that classifies it") by letting two writers' records interleave in one JSONL file without error. This is the same root cause independently flagged by Codex's P1 review comment on line 496.

Exploitability under the documented single acquire → heartbeat/append → release call pattern is narrow (stale-lease adoption / compare-and-set is explicitly out of scope for this PR per the PR description), but the write side of the mutual-exclusion problem has no guard at all, while the read/classify side is carefully double-bound (the two-sided liveness window and its negative tests are solid).

Suggested fix: have lease acquire refuse (or require an explicit adoption flag) when an existing lease at --run-dir currently classifies live, and have partial append take/verify the epoch it expects rather than trusting whatever the lease currently holds on disk.

Fix this →

No other issues found

  • validate_run_id (lines 121-133) correctly rejects empty ids, non-[A-Za-z0-9][A-Za-z0-9_.-]* shapes, and any .. substring. run-state.test.sh's three mutation-based negative tests genuinely prove each check is load-bearing (deleting a check demonstrably lets traversal/absolute paths through), closing path traversal for the one caller-supplied input the script treats as untrusted-by-design.
  • No eval, source, or command substitution over unsanitized input anywhere in the script; all arguments are consumed as literal argv values. Confirmed run-state.test.sh contains no eval/unsafe temp-file handling either — its mktemp -d usage (line 17) is the standard safe pattern.
  • partial append's --record is written via a plain printf '%s\n' "$record" >>"$file" (line 501) — never parsed or evaluated by this script, so a malformed/adversarial value can at most corrupt the JSONL artifact's own readability for a downstream reader (data-integrity concern for --resume parsing, not injection or traversal here). The '{'* prefix check (lines 486-489) is not full JSON validation, but that's a robustness gap, not a security boundary.
  • write_lease_atomic's temp file ($dir/.lease.$$) is written under a directory the script itself just created; a symlink race there requires an attacker who already has write access to the same user's plugin-data directory — the same trust level as the process itself, not a meaningful privilege boundary.
  • epoch/stale_after/skew_grace are all validated as non-negative integers via require_positive_int before being written into the lease or used to build the partial's filename — no injection via those fields.
  • No secrets, tokens, or credentials are handled anywhere in this diff.
  • The doc changes (SKILL.md, both reference/*.md files) only describe invocation of the script and don't assert any security guarantee the script doesn't itself state — the "writes only under the plugin data directory it is given" framing matches the two gaps above rather than introducing new ones.

Summary: two IMPORTANT, plausible-confidence findings, both about mechanism-vs-prose trust boundaries in run-state.sh (unvalidated --run-dir containment; no compare-and-set on lease acquire, letting partial append's epoch binding be redirected). No CRITICAL findings. No injection, secret-handling, or workflow-permission issues found.

@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 6m 31s —— View job


Code Review (correctness / maintainability lane)

  • Gather context (diff, changed files)
  • Read repo CLAUDE.md / REVIEW.md for lane scoping and standards
  • Review run-state.sh for correctness, error handling, maintainability
  • Review run-state.test.sh for test coverage/quality
  • Review SKILL.md / reference doc diffs for consistency with the script
  • Post inline findings + summary

This repo has a claude-security-review workflow, so per REVIEW.md's lane split this review omits security findings (trust boundary, injection, authz) — those belong to the security lane, which has already posted twice on this PR. This pass covers correctness, design, error handling, and test coverage.

Note on verification: git fetch/bash <script>/shellcheck all required interactive approval that wasn't available in this run, so I could not execute run-state.test.sh or shellcheck myself. The Test plan's pass/fail output in the PR description is the author's own claimed verification, not something I independently confirmed — I reviewed the script and tests by reading, not running them.

Important — the applying lock is unimplemented, and unlike the two clauses this PR does disclose, that's not stated anywhere

§3 specifies two run-state mechanisms: the lease (which this PR implements: lease acquire|heartbeat|release|classify) and a separate exclusive advisory lock an applying (--fix) run takes at runs/<state-key>/lock — containing pid, platform start-identity, a start timestamp, and the run id, with its own 30-minute age-based reclaim logic (reference/run-state-and-resumability.md#L29-L65). run-state.sh has no lock subcommand at all — only paths, lease *, and partial append — so taking, refreshing, reclaiming, or releasing that lock is still entirely a model-discipline operation, exactly the "contract read as enforced when nothing enforces it" defect this PR's whole thesis is about closing.

What makes this worth flagging rather than out-of-scope-and-fine: the PR is careful to name the two things it deliberately leaves unimplemented — stale-lease adoption and §7 assembly — in both the script header and a new "What is executable here, and what is not" paragraph it adds right into this same reference doc (reference/run-state-and-resumability.md#L96-L103). That disclosure paragraph is scoped to "everything below" it — i.e. the lease subsection — and the lock, described just above it in the same §3, gets no equivalent statement anywhere in this diff or pre-existing text. SKILL.md still just says "an applying run takes an exclusive advisory lock and refuses rather than queues" (SKILL.md#L134) with no note that this, too, has no executable behind it.

This is pre-existing (the lock was never implemented before this PR either), so it's not a regression — but this diff is the one that establishes the "declare what you don't implement" norm in this exact section, and the lock is the one adjacent mechanism the norm isn't applied to. Extending the "what is executable here, and what is not" paragraph to cover the lock (or adding a one-line sibling note near SKILL.md's lock-posture paragraph) would close it cheaply and keep the PR's own standard applied consistently.

Two inline findings (test coverage / validation)

  • require_positive_int accepts 0 and is reused to validate --epoch, but the lease's documented contract is owner_epoch "starting at 1" — --epoch 0 passes silently. (run-state.sh:267)
  • require_positive_int's rejection path (malformed --stale-after/--skew-grace/--epoch) has no test, positive or negative, unlike validate_run_id which is covered by both ordinary and mutation-based negative tests. (run-state.test.sh:136)

No other issues found

  • write_lease_atomic's write-then-rename keeps every lease write atomic from a reader's perspective, satisfying REVIEW.md's atomicity check for the one place this script writes a record that other code paths read mid-flight.
  • cmd_paths's error path when lib/state-key.sh produces no output is sound: I read lib/state-key.sh directly and confirmed every one of its exit 2 paths happens before its final printf, so cmd_paths's [[ -z "$state_key" ]] check is a real (not just apparent) safety net rather than a race with partial stdout.
  • cmd_lease_heartbeat's max(now, previous) guard against a rewound clock, and cmd_lease_classify's two-sided window, are both logically sound and match what run-state.test.sh exercises by reading.
  • CHANGELOG and plugin.json version bumps (0.37.20.38.0) are consistent with each other and with semver for a new-feature addition.
  • The default threshold change (300s → 1800s) is consistently applied across the script's DEFAULT_STALE_AFTER_S, SKILL.md, and both reference docs — no stale "5 minutes" reference was missed.
    (branch: fix/2280-audit-pass-run-state)

…state

A lease recording `stale_after_s=0` satisfies the staleness test the moment it
is written — born abandoned, and adoptable by `--resume` out from under the run
that just wrote it. §3 now documents `--stale-after` as an operator lever, so the
zero value has to refuse rather than quietly produce a lease no run could keep.
Refused rather than clamped: a clamp hands a caller a window it did not choose
and then reports on it. `--skew-grace 0` stays legal — "tolerate no forward clock
jump" is a coherent choice and inverts nothing.

Also names `lease acquire --epoch <n>` as the adoption seam it is, rather than
leaving an undocumented flag reading as a feature: an adopting run passes the
epoch it won, and the compare-and-set that decides who won is §3's, performed by
the run and not by this script.

Refs #2280 (F5)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 6s —— View job


Security review

  • Gather context (diff, changed files, delta since last review)
  • Review run-state.sh for injection/path-traversal/logic/trust-boundary issues
  • Review run-state.test.sh for unsafe patterns
  • Review doc/reference changes for security-relevant claims
  • Check what changed in the newest commit (zero-staleness-window fix) since the last automated pass
  • Post findings

Scope reviewed: all 7 files changed in this PR at HEAD 75007bc (plugins/claude-config/skills/audit-pass/scripts/run-state.sh, run-state.test.sh, SKILL.md, both reference/*.md, CHANGELOG.md, .claude-plugin/plugin.json). No workflow/Action files changed, so zizmor's lanes (unpinned actions, dangerous triggers, excessive permissions:, template injection) don't apply.

The newest commit (75007bc, "refuse a zero staleness window") only touches --stale-after/--epoch validation (require_int_at_least_one) and documents --epoch as the adoption seam — it does not change either of the two trust-boundary gaps prior automated passes on this PR already identified, which I re-verified directly against the current code rather than assuming carried forward:

IMPORTANT (confidence: plausible) — --run-dir is trusted verbatim by lease * / partial append, unlike paths

cmd_paths validates both segments it derives--run-id via validate_run_id (line 132) and --plugin-data for being absolute (line 230) — before assembling run_dir. But cmd_lease_acquire, heartbeat/release/classify, and cmd_partial_append all accept --run-dir as a raw string with no check that it falls under <plugin-data>/runs/<state-key>/…. Line 300 unconditionally mkdir -p "$run_dir" and writes a lease file at whatever path it's given.

The script's own header ("SCOPE OF WRITES", lines 34-42) and SKILL.md ("Do not derive the run directory or hand-write the lease", SKILL.md:140-146) describe writes as confined to the derived run directory, but that ordering — always pipe paths' output into later commands — is enforced only by the calling skill's prose discipline, not by the script. For a report-only audit skill that processes potentially adversarial/untrusted repository content with Bash/Write still available (disallowed-tools: Edit, NotebookEdit only), a --run-dir redirected by a confused-deputy or prompt-injection-influenced tool call would be honored silently. Impact stays limited (directory creation + small fixed-name text files, no code execution, no credential exposure), so not critical — but it's a real, unaddressed gap between the documented trust boundary and what's mechanically enforced.

Suggested fix: have lease */partial append verify --run-dir is lexically contained under a --plugin-data-rooted runs/ prefix (or take --plugin-data+--run-id and re-derive the path via cmd_paths's own logic) before mkdir -p or writing.

IMPORTANT (confidence: plausible) — lease acquire still has no liveness/compare-and-set check; partial append still binds to whatever epoch is on disk, not the epoch the caller holds

This commit documents lease acquire --epoch <n> as "the adoption seam" and states the compare-and-set "is §3's, performed by the run" — but cmd_lease_acquire still never reads or classifies an existing lease before replacing it (line 300 onward unconditionally overwrites, even if classify on the existing lease would currently return live). cmd_partial_append still reads owner_epoch from the lease file at append time, not the epoch the caller originally acquired — so if a second lease acquire (adopting or otherwise) is ever issued against a run directory whose lease is still live, a prior holder's subsequent partial append calls silently land in the new writer's epoch file, interleaving two writers' records in one JSONL artifact. This is the same root cause Codex's P1 comment (line 524) independently flagged, unchanged by this commit.

This is explicitly declared out of scope by the PR description ("Stale-lease adoption... NOT implemented... carried out by the run itself"), and exploitability under the documented single acquire → heartbeat/append → release pattern is narrow (requires a run-id collision or an out-of-order tool call). Flagging again only because the write side of the mutual-exclusion problem still has zero guard while the read/classify side is carefully double-bound — so, as before, the isolation property is only as strong as caller discipline, not the script, and this commit's own header now says so explicitly (line 31: "The compare-and-set that decides who won is §3's, performed by the run") without changing that the script accepts any --epoch value handed to it with no verification against the lease it's replacing.

Suggested fix (if in scope for a follow-up): have lease acquire refuse (absent an explicit adopt path) when an existing lease at --run-dir currently classifies live, and have partial append verify/bind to the epoch the caller expects rather than trusting whatever the lease currently holds on disk.

No new issues from this commit's changes

  • require_int_at_least_one (lines 162-168) correctly composes require_non_negative_int with a < 1 check; --skew-grace correctly stays on the non-negative-only path so --skew-grace 0 remains legal. No injection or logic inversion introduced — stale_after/epoch are still only ever used as validated integers in arithmetic (delta=$((now - heartbeat))) or written verbatim into the lease as key=value lines, never interpreted by the shell.
  • The two new test blocks in run-state.test.sh (lines 150-169) exercise the refusal paths with the same safe mktemp -d / subshell pattern as the rest of the file — no eval, no unsafe temp-file handling.
  • Doc changes (CHANGELOG.md, reference/run-state-and-resumability.md) only describe the new refusal and the --epoch adoption seam; they don't assert any security guarantee beyond what the script does, and don't overstate --epoch's protection (the surrounding prose is explicit that the CAS itself isn't implemented here).

Carried-forward from prior passes, still holding at this HEAD:

  • validate_run_id (lines 132-144) correctly rejects empty ids, non-[A-Za-z0-9][A-Za-z0-9_.-]* shapes, and any .. substring, with load-bearing mutation-based negative tests. Path traversal via --run-id remains closed.
  • No eval, source, or command substitution over unsanitized input anywhere in the script.
  • partial append's --record is a plain printf '%s\n' "$record" >>"$file" — never parsed/evaluated, so a malformed value corrupts JSONL readability for a downstream reader at worst, not a security boundary.
  • write_lease_atomic's temp file ($dir/.lease.$$) is written under a directory the script just created — a symlink race there needs the same trust level as the process itself.
  • No secrets, tokens, or credentials handled anywhere in this diff.

Summary: two IMPORTANT, plausible-confidence findings, both unchanged mechanism-vs-prose trust-boundary gaps (unvalidated --run-dir containment; no compare-and-set on lease acquire, letting partial append's epoch binding be redirected). No CRITICAL findings and nothing newly introduced by the latest commit. No injection, secret-handling, or workflow-permission issues found.

@kyle-sexton kyle-sexton left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not merge without orchestrator sign-off on the two spec reductions. Leaving this as an
unresolved thread rather than only in the PR body, because this repo has two recorded instances of an
explicitly-blocked PR being merged by another actor, and the durable form of a blocking finding is a
thread that outlives the reviewing session.

Nothing here is unsafe and nothing must-not-ship. What needs a human is that two clauses of a
shipped contract were reduced rather than implemented
, and a reduction is a judgment call a silent
merge would ship unreviewed:

  1. The 60-second wall-clock heartbeat became boundary-driven refresh, and the default staleness
    threshold moved 5 minutes to 30.
    My reasoning: a skill acts between tool calls and has no timer,
    so the timed cadence named a mechanism no run could keep — and the derived 5-minute threshold is
    shorter than a single delegated lane can run, which classifies a running pass as abandoned and
    lets --resume adopt a live run's artifact. The threshold now travels in the lease so classify
    reads what was written rather than assuming a constant. The alternative I rejected was spawning a
    detached heartbeat process: an orphaned one pins the lease live forever, which is strictly worse
    than the defect.
  2. §5's separate "run manifest" became the partial's own lane records. §7 already required that
    completion state be derivable from the partial "rather than tracked beside it and able to disagree
    with it", and a manifest beside the partial is exactly what can disagree. This also closes the
    second link in F12's chain — making the partial real while leaving completion state in a file
    nothing writes would have moved the defect, not fixed it.

Also flagged for the reviewer's attention, though I believe it is correct: stale-lease adoption
(owner_epoch compare-and-set) and §7 assembly are deliberately left unimplemented
, and §3/§7 now
say so explicitly rather than reading as machinery. If the orchestrator wants those built rather than
declared, this PR is the wrong shape and should be split.

Resolve this thread once those three calls are confirmed.

Comment thread plugins/claude-config/skills/audit-pass/scripts/run-state.sh
Comment thread plugins/claude-config/skills/audit-pass/scripts/run-state.test.sh
@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown

Claude has reviewed this PR 2 times. The lane skips further automatic reviews after 5; deleting this comment resets the count.

@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 6m 13s —— View job


Code review

  • Gather context (diff, changed files, CLAUDE.md/REVIEW.md guidelines)
  • Review run-state.sh for correctness, error handling, maintainability
  • Review run-state.test.sh for test coverage/quality
  • Review SKILL.md and reference doc changes for consistency
  • Cross-check existing bot findings (Cursor/Codex/security lane) to avoid duplicating security scope
  • Post findings

Scope. This repo runs a dedicated security lane (.github/workflows/claude-security-review.yml), so per REVIEW.md this lane owns correctness, design, error handling, tests, and documentation, and leaves security to that lane. The --run-dir containment gap and the lease acquire/epoch compare-and-set gap already flagged by Cursor, the security-review bot, and Codex are security-scope findings — not repeated here.

Posted 3 inline findings on this pass, all correctness/design/test-coverage, none security:

  1. 🔴 ImportantSKILL.md:145: the canonical paths invocation omits --root, so the state key falls back to the Bash tool's ambient cwd instead of the target root Phase 0 just resolved — in tension with this same file's own "never the working directory" invariant (lines 56-58), and the one invocation shape run-state.test.sh never actually exercises (every test call passes --root).
  2. 🟡 Nitrun-state.sh:352: cmd_lease_heartbeat's clock-rewind guard pins heartbeat_at to the old value but always stamps heartbeat_at_iso with the current time, so the two fields can describe different instants on disk after a rewind. No functional impact (classify only reads the numeric field), but it's exactly the class of bug the 3x-duplicated lease-body construction (lines 305, 354, 396) invites — a shared write_lease helper would remove it structurally.
  3. 🟡 Nitrun-state.test.sh:51: CLAUDE_PLUGIN_ROOT is pinned in every test invocation, so cmd_paths's environment-fallback path derivation (run-state.sh:238) has zero coverage anywhere in the suite.

Everything else checked, no issues:

  • Version bump (0.37.20.38.0) and CHANGELOG entry are consistent with a new executable surface (minor, not patch), and the documented defaults (stale_after_s=1800, skew_grace_s=60) match the code exactly.
  • usage() text, the command dispatch table, and exit-code contract (0 for a classification, 2 for usage/validation errors) all match actual behavior.
  • The three negative tests (.. rejection, segment-shape rejection, lower-bound skew rejection) each self-verify their sed target still matches before trusting the mutated copy — confirmed each target string (e.g. "must not contain") is unique in run-state.sh, so the mutation touches only the intended line.
  • Atomicity (REVIEW.md's always-check item): the lease is replaced via temp-file + mv -f (write_lease_atomic), so a reader never observes a half-written lease and a crash mid-write leaves the prior lease intact.
  • Doc cross-references between SKILL.md, both reference/*.md files, and the script's own header comments are internally consistent (same defaults, same "script enforces X, run enforces Y" split stated in each place it's relevant).

Note: I did not independently run run-state.test.sh, shellcheck, or the repo's portability/changelog gates myself — the Test Plan's "52 checks passed" and gate output in the PR description is the author's own claimed verification, not something I confirmed with a tool.
· branch fix/2280-audit-pass-run-state

Comment thread plugins/claude-config/skills/audit-pass/SKILL.md
Comment thread plugins/claude-config/skills/audit-pass/scripts/run-state.sh
Comment thread plugins/claude-config/skills/audit-pass/scripts/run-state.test.sh
@kyle-sexton
kyle-sexton merged commit e4501a0 into main Aug 12, 2026
37 checks passed
@kyle-sexton
kyle-sexton deleted the fix/2280-audit-pass-run-state branch August 12, 2026 13:19
kyle-sexton added a commit that referenced this pull request Aug 12, 2026
…ate records, pin the write tree (#2446)

## Summary

**`claude-config` 0.38.0 is on `main` carrying three defects in the
script it shipped.** They were
found by review on #2441; that PR merged at 13:19:55Z while the fixes
were still being written, so
they ship here as 0.38.1. Anyone on 0.38.0 has all three.

All three are one family, and it is the family this batch keeps finding:
**a control that does not
enforce what its surface claims.**

**1. The partial was named for the lease's *current* epoch, not the
writer's.** `partial append` read
`owner_epoch` out of the lease at append time. That defeats precisely
the isolation
`run-state-and-resumability.md` §3 describes: a stale holder that wakes
after an adopter has
incremented the epoch reads the *adopter's* value and appends into the
*adopter's* file, so two
writers interleave under one attempt ordinal — which §3 itself calls
"the one failure the attempt
machinery cannot absorb". The script was contradicting the paragraph it
was written to enforce.

`partial append --epoch <held>` now names the writer's own file whatever
the lease says, and prints
`FENCED` on stderr when the two differ so the run aborts on the signal
rather than corrupting the
artifact quietly. Omitting the flag still falls back to the lease's
epoch, correct only for a run
whose epoch nothing has moved — and that is now stated rather than
assumed.

**2. `{bad json}` was accepted and appended permanently.** The check was
`case "$record" in '{'*)`,
which passes any string starting with a brace. A malformed row in an
append-only artifact cannot be
taken back, and `--resume` and assembly are its only readers, so a
quoting slip in the caller cost the
run's whole persisted state rather than one record.

Records are now verified as well-formed single-line JSON objects. `jq`
decides where it is installed;
where it is not, a scan that tracks string context and escape sequences
still rejects `{bad json}`, a
truncated row, and an unbalanced one. **`jq` is deliberately not a hard
requirement** — this is the
run's state-persistence path, and failing it closed on a missing
optional tool would cost the artifact
the check exists to protect. Both rungs are asserted; the fallback runs
with a `PATH` holding only
`bash`.

**3. `lease acquire` created and wrote into any `--run-dir` it was
handed.** A wrong or invented run
directory — the target root, say — was created and had a `lease` written
into it. This skill keeps
Bash specifically for state writes *while promising that a bare audit
writes nothing into the target*,
so that promise was enforceable only by the caller getting the argument
right. `acquire` is the only
command that creates a directory, so it is where the write tree is
pinned: it now requires
`--plugin-data` and refuses any run directory outside
`<plugin-data>/runs/`. Every later command
operates on a directory `acquire` already validated.

Version: `claude-config` **0.38.0 to 0.38.1**. The shipped `## [0.38.0]`
section is untouched —
0.38.1 is additive and says outright that 0.38.0 carries all three.

## Test plan

**Fail-before / pass-after, per finding.** Each was reproduced against
the merged 0.38.0 script
before the fix, and each ships an assertion that fails without it.

Verbatim transcript. `v0380.sh` is `git show
origin/main:.../run-state.sh` — the script as merged. The
lease is moved to `owner_epoch=9` as an adopter would leave it, while
the writer still holds 3.

```
--- 1. epoch binding, 0.38.0 (writer holds 3, lease says 9) ---
/tmp/tmp.RpfHhnqzmO/data/runs/demo/r1/findings.partial.9.jsonl
rc=0
--- 2. malformed record, 0.38.0 ---
/tmp/tmp.RpfHhnqzmO/data/runs/demo/r1/findings.partial.9.jsonl
rc=0
--- 3. write-tree containment, 0.38.0 ---
/tmp/tmp.RpfHhnqzmO/target-repo/.claude/lease
rc=0
lease
^ the lease was written into a non-plugin tree
=== now the fixed script ===
--- 1. fixed ---
run-state.sh: FENCED — lease owner_epoch is 9 but this writer holds 3; appending to the writer's own epoch file and this run must abort
/tmp/tmp.RpfHhnqzmO/data/runs/demo/r1/findings.partial.3.jsonl
rc=0
--- 2. fixed ---
run-state.sh: --record must be a well-formed single-line JSON object: {bad json}
rc=2
--- 3. fixed ---
run-state.sh: --run-dir is not under $plugin-data/runs/ — refusing to write outside the plugin's own tree: /tmp/tmp.RpfHhnqzmO/target-repo2/.claude
rc=2
```

Read line 2 against line 12: on 0.38.0 the record the writer holding
epoch 3 appended landed in
`findings.partial.**9**.jsonl` — the adopter's file. On the fix it lands
in `.3.` and the fence is
announced. Line 4 is `{bad json}` accepted and appended at `rc=0`; line
7 is a lease written into
`target-repo/.claude`, a directory the script created outside the
plugin's tree.

**Suite — 73 checks, up from 57:**

```
$ bash plugins/claude-config/skills/audit-pass/scripts/run-state.test.sh
PASS: a run dir outside <plugin-data>/runs/ is refused
PASS: the refusal says it will not write outside the plugin tree
PASS: the refused run dir was not created
PASS: a run dir containing '..' is refused
PASS: acquire without --plugin-data has nothing to check containment against, and refuses
PASS: a balanced-but-malformed record is refused, not appended
PASS: a truncated record is refused
PASS: a brace inside a string does not fool the check
PASS: and the malformed records left the artifact untouched
PASS: without jq, a balanced-but-malformed record is still refused
PASS: without jq, a truncated record is still refused
PASS: without jq, a brace inside a string does not fool the fallback
PASS: without jq, an empty object is accepted
PASS: a fenced writer appends to its own epoch file, not the adopter's
PASS: and the fence is reported rather than swallowed
PASS: omitting --epoch still falls back to the lease's current epoch
...
All 73 checks passed.
```

The three mutation-based negative tests from 0.38.0 still pass unchanged
— each deletes exactly one
check from a copy of the script and asserts the mutated copy reaches the
outcome the real one refuses.

**Repo gates, locally:**

```
$ shellcheck --rcfile .shellcheckrc -S info plugins/claude-config/skills/audit-pass/scripts/*.sh
(clean)
$ typos --config _typos.toml plugins/claude-config/
(clean)
$ bash scripts/check-changed-skills.sh origin/main
CHECK-SKILL audit-pass: PASS — 0 errors, 2 warning(s)
$ bash scripts/check-changelog-parity.sh --check-preserved origin/main
All 1 changed changelog(s) preserve every version heading they carried at e4501a0 (68 compared).
$ bash scripts/check-changelog-parity.sh --check-bump origin/main
Every plugin whose version changed vs origin/main has a '## [<version>]' CHANGELOG.md entry.
$ npx markdownlint-cli2 "plugins/claude-config/skills/audit-pass/**/*.md" "plugins/claude-config/CHANGELOG.md"
Summary: 0 issues in 0 files
```

Both `.sh` files carry mode `100755` — the `exec-bit` sub-check of
`hygiene` fails on `100644`, and it
is invisible to every local linter.

## Review rounds on this PR, and what they changed

Seven further findings landed after the first push. All are fixed here,
each with an assertion, and
every one is the same class again — a control not enforcing what its
surface claims. Recorded because
three of them were in *my own fix for that class*.

- **🔴 `FENCED` was advisory only.** The script printed "this run must
abort" and returned 0, so the
abort depended on the caller spotting a substring on stderr. A fenced
append now **exits 3**; the
record still lands in the writer's own epoch file, and `SKILL.md` Phase
3 says what to do with the
  code. The test asserts the exit code, not just the message.
- **🔴 The symlink fix broke the very first run.** `pwd -P` cannot
resolve a directory that is not
there, so canonicalizing `--plugin-data` before creating it made `lease
acquire` fail on a plugin's
first-ever run — the only run whose data root has never existed. **Every
test in the file pre-makes
that directory, so none could see it.** Reproduced against the prior
commit (`--plugin-data cannot
be resolved`, `rc=2`), fixed, and a test now exercises the fresh-install
path.
- **P1: the no-parser rung over-claimed.** `{"a" garbage}` balances and
opens with a quoted key, and
no non-parser catches it. `python3` is now the second definitive rung
after `jq`, and where neither
exists the structural scan **announces on stderr that it checked
structurally only** rather than
  passing for a validator.
- **P2: containment was lexical, so a symlink defeated it.** Both sides
are canonicalized with
`pwd -P` — `--run-dir` through its deepest existing ancestor. Tests
assert the escape is refused,
that no lease was written through the link, and that a symlink resolving
back inside still works.
  The arm skips on Windows (`ln -s` refused) and **passes on CI**.
- **P2: `report-location-and-schema.md` §7 still described the old
append contract**, so an audit
following that page after an adoption would have omitted `--epoch` and
reinstated the interleaving.
  Both surfaces now carry the same command.
- **🟡 check-then-act window** between symlink resolution and `mkdir`:
accepted and now recorded in
the code as a disclosed residual. Closing it needs an atomic
create-and-verify no portable shell
offers, and the attacker would already need write access inside the
plugin's own data directory.
- **🟡 a shared validator printed a staleness-specific reason at an
`--epoch` rejection.** The reason
  is now an argument.

Suite: **81 checks**, all passing. All six bot threads are replied to
and resolved; the one open
thread is my own do-not-merge marker.

## Related

Refs #2280

No linked issue closes here. #2280 was closed by #2441's merge and its
four live rows (F3, F5, F12,
F13) are genuinely retired by the mechanism that shipped; this PR
repairs three defects *in that
mechanism* rather than reopening a row, so it deliberately carries no
closing keyword.

Follow-up to #2441 (`claude-config` 0.38.0). Origin: handoff-inbox item
`20260811-020411-claude-config-audit-pass-report-path-inside-scan-set`.

**Note for the reviewer, recorded rather than left implicit.** #2441 was
merged by another actor
while it carried an explicit "do not merge" and an unresolved review
comment naming two deliberate
spec reductions for sign-off. Nothing shipped that is unsafe, but three
real defects reached `main`
that would not have. Those two reductions — the 60-second wall-clock
heartbeat becoming
boundary-driven refresh with the thresholds recorded in the lease, and
§5's separate run manifest
becoming the partial's own lane records — are now live on `main`
unreviewed and still want a human
read. They are described in #2441's body and in the `## [0.38.0]`
CHANGELOG entry.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
kyle-sexton added a commit that referenced this pull request Aug 12, 2026
…ate records, pin the write tree (#2446)

## Summary

**`claude-config` 0.38.0 is on `main` carrying three defects in the
script it shipped.** They were
found by review on #2441; that PR merged at 13:19:55Z while the fixes
were still being written, so
they ship here as 0.38.1. Anyone on 0.38.0 has all three.

All three are one family, and it is the family this batch keeps finding:
**a control that does not
enforce what its surface claims.**

**1. The partial was named for the lease's *current* epoch, not the
writer's.** `partial append` read
`owner_epoch` out of the lease at append time. That defeats precisely
the isolation
`run-state-and-resumability.md` §3 describes: a stale holder that wakes
after an adopter has
incremented the epoch reads the *adopter's* value and appends into the
*adopter's* file, so two
writers interleave under one attempt ordinal — which §3 itself calls
"the one failure the attempt
machinery cannot absorb". The script was contradicting the paragraph it
was written to enforce.

`partial append --epoch <held>` now names the writer's own file whatever
the lease says, and prints
`FENCED` on stderr when the two differ so the run aborts on the signal
rather than corrupting the
artifact quietly. Omitting the flag still falls back to the lease's
epoch, correct only for a run
whose epoch nothing has moved — and that is now stated rather than
assumed.

**2. `{bad json}` was accepted and appended permanently.** The check was
`case "$record" in '{'*)`,
which passes any string starting with a brace. A malformed row in an
append-only artifact cannot be
taken back, and `--resume` and assembly are its only readers, so a
quoting slip in the caller cost the
run's whole persisted state rather than one record.

Records are now verified as well-formed single-line JSON objects. `jq`
decides where it is installed;
where it is not, a scan that tracks string context and escape sequences
still rejects `{bad json}`, a
truncated row, and an unbalanced one. **`jq` is deliberately not a hard
requirement** — this is the
run's state-persistence path, and failing it closed on a missing
optional tool would cost the artifact
the check exists to protect. Both rungs are asserted; the fallback runs
with a `PATH` holding only
`bash`.

**3. `lease acquire` created and wrote into any `--run-dir` it was
handed.** A wrong or invented run
directory — the target root, say — was created and had a `lease` written
into it. This skill keeps
Bash specifically for state writes *while promising that a bare audit
writes nothing into the target*,
so that promise was enforceable only by the caller getting the argument
right. `acquire` is the only
command that creates a directory, so it is where the write tree is
pinned: it now requires
`--plugin-data` and refuses any run directory outside
`<plugin-data>/runs/`. Every later command
operates on a directory `acquire` already validated.

Version: `claude-config` **0.38.0 to 0.38.1**. The shipped `## [0.38.0]`
section is untouched —
0.38.1 is additive and says outright that 0.38.0 carries all three.

## Test plan

**Fail-before / pass-after, per finding.** Each was reproduced against
the merged 0.38.0 script
before the fix, and each ships an assertion that fails without it.

Verbatim transcript. `v0380.sh` is `git show
origin/main:.../run-state.sh` — the script as merged. The
lease is moved to `owner_epoch=9` as an adopter would leave it, while
the writer still holds 3.

```
--- 1. epoch binding, 0.38.0 (writer holds 3, lease says 9) ---
/tmp/tmp.RpfHhnqzmO/data/runs/demo/r1/findings.partial.9.jsonl
rc=0
--- 2. malformed record, 0.38.0 ---
/tmp/tmp.RpfHhnqzmO/data/runs/demo/r1/findings.partial.9.jsonl
rc=0
--- 3. write-tree containment, 0.38.0 ---
/tmp/tmp.RpfHhnqzmO/target-repo/.claude/lease
rc=0
lease
^ the lease was written into a non-plugin tree
=== now the fixed script ===
--- 1. fixed ---
run-state.sh: FENCED — lease owner_epoch is 9 but this writer holds 3; appending to the writer's own epoch file and this run must abort
/tmp/tmp.RpfHhnqzmO/data/runs/demo/r1/findings.partial.3.jsonl
rc=0
--- 2. fixed ---
run-state.sh: --record must be a well-formed single-line JSON object: {bad json}
rc=2
--- 3. fixed ---
run-state.sh: --run-dir is not under $plugin-data/runs/ — refusing to write outside the plugin's own tree: /tmp/tmp.RpfHhnqzmO/target-repo2/.claude
rc=2
```

Read line 2 against line 12: on 0.38.0 the record the writer holding
epoch 3 appended landed in
`findings.partial.**9**.jsonl` — the adopter's file. On the fix it lands
in `.3.` and the fence is
announced. Line 4 is `{bad json}` accepted and appended at `rc=0`; line
7 is a lease written into
`target-repo/.claude`, a directory the script created outside the
plugin's tree.

**Suite — 73 checks, up from 57:**

```
$ bash plugins/claude-config/skills/audit-pass/scripts/run-state.test.sh
PASS: a run dir outside <plugin-data>/runs/ is refused
PASS: the refusal says it will not write outside the plugin tree
PASS: the refused run dir was not created
PASS: a run dir containing '..' is refused
PASS: acquire without --plugin-data has nothing to check containment against, and refuses
PASS: a balanced-but-malformed record is refused, not appended
PASS: a truncated record is refused
PASS: a brace inside a string does not fool the check
PASS: and the malformed records left the artifact untouched
PASS: without jq, a balanced-but-malformed record is still refused
PASS: without jq, a truncated record is still refused
PASS: without jq, a brace inside a string does not fool the fallback
PASS: without jq, an empty object is accepted
PASS: a fenced writer appends to its own epoch file, not the adopter's
PASS: and the fence is reported rather than swallowed
PASS: omitting --epoch still falls back to the lease's current epoch
...
All 73 checks passed.
```

The three mutation-based negative tests from 0.38.0 still pass unchanged
— each deletes exactly one
check from a copy of the script and asserts the mutated copy reaches the
outcome the real one refuses.

**Repo gates, locally:**

```
$ shellcheck --rcfile .shellcheckrc -S info plugins/claude-config/skills/audit-pass/scripts/*.sh
(clean)
$ typos --config _typos.toml plugins/claude-config/
(clean)
$ bash scripts/check-changed-skills.sh origin/main
CHECK-SKILL audit-pass: PASS — 0 errors, 2 warning(s)
$ bash scripts/check-changelog-parity.sh --check-preserved origin/main
All 1 changed changelog(s) preserve every version heading they carried at e4501a0 (68 compared).
$ bash scripts/check-changelog-parity.sh --check-bump origin/main
Every plugin whose version changed vs origin/main has a '## [<version>]' CHANGELOG.md entry.
$ npx markdownlint-cli2 "plugins/claude-config/skills/audit-pass/**/*.md" "plugins/claude-config/CHANGELOG.md"
Summary: 0 issues in 0 files
```

Both `.sh` files carry mode `100755` — the `exec-bit` sub-check of
`hygiene` fails on `100644`, and it
is invisible to every local linter.

## Review rounds on this PR, and what they changed

Seven further findings landed after the first push. All are fixed here,
each with an assertion, and
every one is the same class again — a control not enforcing what its
surface claims. Recorded because
three of them were in *my own fix for that class*.

- **🔴 `FENCED` was advisory only.** The script printed "this run must
abort" and returned 0, so the
abort depended on the caller spotting a substring on stderr. A fenced
append now **exits 3**; the
record still lands in the writer's own epoch file, and `SKILL.md` Phase
3 says what to do with the
  code. The test asserts the exit code, not just the message.
- **🔴 The symlink fix broke the very first run.** `pwd -P` cannot
resolve a directory that is not
there, so canonicalizing `--plugin-data` before creating it made `lease
acquire` fail on a plugin's
first-ever run — the only run whose data root has never existed. **Every
test in the file pre-makes
that directory, so none could see it.** Reproduced against the prior
commit (`--plugin-data cannot
be resolved`, `rc=2`), fixed, and a test now exercises the fresh-install
path.
- **P1: the no-parser rung over-claimed.** `{"a" garbage}` balances and
opens with a quoted key, and
no non-parser catches it. `python3` is now the second definitive rung
after `jq`, and where neither
exists the structural scan **announces on stderr that it checked
structurally only** rather than
  passing for a validator.
- **P2: containment was lexical, so a symlink defeated it.** Both sides
are canonicalized with
`pwd -P` — `--run-dir` through its deepest existing ancestor. Tests
assert the escape is refused,
that no lease was written through the link, and that a symlink resolving
back inside still works.
  The arm skips on Windows (`ln -s` refused) and **passes on CI**.
- **P2: `report-location-and-schema.md` §7 still described the old
append contract**, so an audit
following that page after an adoption would have omitted `--epoch` and
reinstated the interleaving.
  Both surfaces now carry the same command.
- **🟡 check-then-act window** between symlink resolution and `mkdir`:
accepted and now recorded in
the code as a disclosed residual. Closing it needs an atomic
create-and-verify no portable shell
offers, and the attacker would already need write access inside the
plugin's own data directory.
- **🟡 a shared validator printed a staleness-specific reason at an
`--epoch` rejection.** The reason
  is now an argument.

Suite: **81 checks**, all passing. All six bot threads are replied to
and resolved; the one open
thread is my own do-not-merge marker.

## Related

Refs #2280

No linked issue closes here. #2280 was closed by #2441's merge and its
four live rows (F3, F5, F12,
F13) are genuinely retired by the mechanism that shipped; this PR
repairs three defects *in that
mechanism* rather than reopening a row, so it deliberately carries no
closing keyword.

Follow-up to #2441 (`claude-config` 0.38.0). Origin: handoff-inbox item
`20260811-020411-claude-config-audit-pass-report-path-inside-scan-set`.

**Note for the reviewer, recorded rather than left implicit.** #2441 was
merged by another actor
while it carried an explicit "do not merge" and an unresolved review
comment naming two deliberate
spec reductions for sign-off. Nothing shipped that is unsafe, but three
real defects reached `main`
that would not have. Those two reductions — the 60-second wall-clock
heartbeat becoming
boundary-driven refresh with the thresholds recorded in the lease, and
§5's separate run manifest
becoming the partial's own lane records — are now live on `main`
unreviewed and still want a human
read. They are described in #2441's body and in the `## [0.38.0]`
CHANGELOG entry.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
kyle-sexton added a commit that referenced this pull request Aug 12, 2026
…ate records, pin the write tree (#2446)

## Summary

**`claude-config` 0.38.0 is on `main` carrying three defects in the
script it shipped.** They were
found by review on #2441; that PR merged at 13:19:55Z while the fixes
were still being written, so
they ship here as 0.38.1. Anyone on 0.38.0 has all three.

All three are one family, and it is the family this batch keeps finding:
**a control that does not
enforce what its surface claims.**

**1. The partial was named for the lease's *current* epoch, not the
writer's.** `partial append` read
`owner_epoch` out of the lease at append time. That defeats precisely
the isolation
`run-state-and-resumability.md` §3 describes: a stale holder that wakes
after an adopter has
incremented the epoch reads the *adopter's* value and appends into the
*adopter's* file, so two
writers interleave under one attempt ordinal — which §3 itself calls
"the one failure the attempt
machinery cannot absorb". The script was contradicting the paragraph it
was written to enforce.

`partial append --epoch <held>` now names the writer's own file whatever
the lease says, and prints
`FENCED` on stderr when the two differ so the run aborts on the signal
rather than corrupting the
artifact quietly. Omitting the flag still falls back to the lease's
epoch, correct only for a run
whose epoch nothing has moved — and that is now stated rather than
assumed.

**2. `{bad json}` was accepted and appended permanently.** The check was
`case "$record" in '{'*)`,
which passes any string starting with a brace. A malformed row in an
append-only artifact cannot be
taken back, and `--resume` and assembly are its only readers, so a
quoting slip in the caller cost the
run's whole persisted state rather than one record.

Records are now verified as well-formed single-line JSON objects. `jq`
decides where it is installed;
where it is not, a scan that tracks string context and escape sequences
still rejects `{bad json}`, a
truncated row, and an unbalanced one. **`jq` is deliberately not a hard
requirement** — this is the
run's state-persistence path, and failing it closed on a missing
optional tool would cost the artifact
the check exists to protect. Both rungs are asserted; the fallback runs
with a `PATH` holding only
`bash`.

**3. `lease acquire` created and wrote into any `--run-dir` it was
handed.** A wrong or invented run
directory — the target root, say — was created and had a `lease` written
into it. This skill keeps
Bash specifically for state writes *while promising that a bare audit
writes nothing into the target*,
so that promise was enforceable only by the caller getting the argument
right. `acquire` is the only
command that creates a directory, so it is where the write tree is
pinned: it now requires
`--plugin-data` and refuses any run directory outside
`<plugin-data>/runs/`. Every later command
operates on a directory `acquire` already validated.

Version: `claude-config` **0.38.0 to 0.38.1**. The shipped `## [0.38.0]`
section is untouched —
0.38.1 is additive and says outright that 0.38.0 carries all three.

## Test plan

**Fail-before / pass-after, per finding.** Each was reproduced against
the merged 0.38.0 script
before the fix, and each ships an assertion that fails without it.

Verbatim transcript. `v0380.sh` is `git show
origin/main:.../run-state.sh` — the script as merged. The
lease is moved to `owner_epoch=9` as an adopter would leave it, while
the writer still holds 3.

```
--- 1. epoch binding, 0.38.0 (writer holds 3, lease says 9) ---
/tmp/tmp.RpfHhnqzmO/data/runs/demo/r1/findings.partial.9.jsonl
rc=0
--- 2. malformed record, 0.38.0 ---
/tmp/tmp.RpfHhnqzmO/data/runs/demo/r1/findings.partial.9.jsonl
rc=0
--- 3. write-tree containment, 0.38.0 ---
/tmp/tmp.RpfHhnqzmO/target-repo/.claude/lease
rc=0
lease
^ the lease was written into a non-plugin tree
=== now the fixed script ===
--- 1. fixed ---
run-state.sh: FENCED — lease owner_epoch is 9 but this writer holds 3; appending to the writer's own epoch file and this run must abort
/tmp/tmp.RpfHhnqzmO/data/runs/demo/r1/findings.partial.3.jsonl
rc=0
--- 2. fixed ---
run-state.sh: --record must be a well-formed single-line JSON object: {bad json}
rc=2
--- 3. fixed ---
run-state.sh: --run-dir is not under $plugin-data/runs/ — refusing to write outside the plugin's own tree: /tmp/tmp.RpfHhnqzmO/target-repo2/.claude
rc=2
```

Read line 2 against line 12: on 0.38.0 the record the writer holding
epoch 3 appended landed in
`findings.partial.**9**.jsonl` — the adopter's file. On the fix it lands
in `.3.` and the fence is
announced. Line 4 is `{bad json}` accepted and appended at `rc=0`; line
7 is a lease written into
`target-repo/.claude`, a directory the script created outside the
plugin's tree.

**Suite — 73 checks, up from 57:**

```
$ bash plugins/claude-config/skills/audit-pass/scripts/run-state.test.sh
PASS: a run dir outside <plugin-data>/runs/ is refused
PASS: the refusal says it will not write outside the plugin tree
PASS: the refused run dir was not created
PASS: a run dir containing '..' is refused
PASS: acquire without --plugin-data has nothing to check containment against, and refuses
PASS: a balanced-but-malformed record is refused, not appended
PASS: a truncated record is refused
PASS: a brace inside a string does not fool the check
PASS: and the malformed records left the artifact untouched
PASS: without jq, a balanced-but-malformed record is still refused
PASS: without jq, a truncated record is still refused
PASS: without jq, a brace inside a string does not fool the fallback
PASS: without jq, an empty object is accepted
PASS: a fenced writer appends to its own epoch file, not the adopter's
PASS: and the fence is reported rather than swallowed
PASS: omitting --epoch still falls back to the lease's current epoch
...
All 73 checks passed.
```

The three mutation-based negative tests from 0.38.0 still pass unchanged
— each deletes exactly one
check from a copy of the script and asserts the mutated copy reaches the
outcome the real one refuses.

**Repo gates, locally:**

```
$ shellcheck --rcfile .shellcheckrc -S info plugins/claude-config/skills/audit-pass/scripts/*.sh
(clean)
$ typos --config _typos.toml plugins/claude-config/
(clean)
$ bash scripts/check-changed-skills.sh origin/main
CHECK-SKILL audit-pass: PASS — 0 errors, 2 warning(s)
$ bash scripts/check-changelog-parity.sh --check-preserved origin/main
All 1 changed changelog(s) preserve every version heading they carried at e4501a0 (68 compared).
$ bash scripts/check-changelog-parity.sh --check-bump origin/main
Every plugin whose version changed vs origin/main has a '## [<version>]' CHANGELOG.md entry.
$ npx markdownlint-cli2 "plugins/claude-config/skills/audit-pass/**/*.md" "plugins/claude-config/CHANGELOG.md"
Summary: 0 issues in 0 files
```

Both `.sh` files carry mode `100755` — the `exec-bit` sub-check of
`hygiene` fails on `100644`, and it
is invisible to every local linter.

## Review rounds on this PR, and what they changed

Seven further findings landed after the first push. All are fixed here,
each with an assertion, and
every one is the same class again — a control not enforcing what its
surface claims. Recorded because
three of them were in *my own fix for that class*.

- **🔴 `FENCED` was advisory only.** The script printed "this run must
abort" and returned 0, so the
abort depended on the caller spotting a substring on stderr. A fenced
append now **exits 3**; the
record still lands in the writer's own epoch file, and `SKILL.md` Phase
3 says what to do with the
  code. The test asserts the exit code, not just the message.
- **🔴 The symlink fix broke the very first run.** `pwd -P` cannot
resolve a directory that is not
there, so canonicalizing `--plugin-data` before creating it made `lease
acquire` fail on a plugin's
first-ever run — the only run whose data root has never existed. **Every
test in the file pre-makes
that directory, so none could see it.** Reproduced against the prior
commit (`--plugin-data cannot
be resolved`, `rc=2`), fixed, and a test now exercises the fresh-install
path.
- **P1: the no-parser rung over-claimed.** `{"a" garbage}` balances and
opens with a quoted key, and
no non-parser catches it. `python3` is now the second definitive rung
after `jq`, and where neither
exists the structural scan **announces on stderr that it checked
structurally only** rather than
  passing for a validator.
- **P2: containment was lexical, so a symlink defeated it.** Both sides
are canonicalized with
`pwd -P` — `--run-dir` through its deepest existing ancestor. Tests
assert the escape is refused,
that no lease was written through the link, and that a symlink resolving
back inside still works.
  The arm skips on Windows (`ln -s` refused) and **passes on CI**.
- **P2: `report-location-and-schema.md` §7 still described the old
append contract**, so an audit
following that page after an adoption would have omitted `--epoch` and
reinstated the interleaving.
  Both surfaces now carry the same command.
- **🟡 check-then-act window** between symlink resolution and `mkdir`:
accepted and now recorded in
the code as a disclosed residual. Closing it needs an atomic
create-and-verify no portable shell
offers, and the attacker would already need write access inside the
plugin's own data directory.
- **🟡 a shared validator printed a staleness-specific reason at an
`--epoch` rejection.** The reason
  is now an argument.

Suite: **81 checks**, all passing. All six bot threads are replied to
and resolved; the one open
thread is my own do-not-merge marker.

## Related

Refs #2280

No linked issue closes here. #2280 was closed by #2441's merge and its
four live rows (F3, F5, F12,
F13) are genuinely retired by the mechanism that shipped; this PR
repairs three defects *in that
mechanism* rather than reopening a row, so it deliberately carries no
closing keyword.

Follow-up to #2441 (`claude-config` 0.38.0). Origin: handoff-inbox item
`20260811-020411-claude-config-audit-pass-report-path-inside-scan-set`.

**Note for the reviewer, recorded rather than left implicit.** #2441 was
merged by another actor
while it carried an explicit "do not merge" and an unresolved review
comment naming two deliberate
spec reductions for sign-off. Nothing shipped that is unsafe, but three
real defects reached `main`
that would not have. Those two reductions — the 60-second wall-clock
heartbeat becoming
boundary-driven refresh with the thresholds recorded in the lease, and
§5's separate run manifest
becoming the partial's own lane records — are now live on `main`
unreviewed and still want a human
read. They are described in #2441's body and in the `## [0.38.0]`
CHANGELOG entry.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant